/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch.attribs;

import java.util.ArrayList;
import java.util.List;

import milk.jpatch.CPoolMap;
import milk.jpatch.Util;

import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ParameterAnnotationEntry;
import org.apache.bcel.classfile.ParameterAnnotations;
import org.apache.bcel.classfile.RuntimeInvisibleParameterAnnotations;
import org.apache.bcel.classfile.RuntimeVisibleParameterAnnotations;

/**
 * Patches a "RuntimeVisibleParameterAnnotations" or
 * "RuntimeInvisibleParameterAnnotations" attribute.
 * 
 * Merges with previous.
 * @author Thomas Kerber
 * @version 1.0.0
 */
public class ParameterAnnotationsPatch extends AttributePatch{
    
    private static final long serialVersionUID = -7407590930928616809L;
    
    /**
     * The annotations to add.
     * 
     * (First dimension: parameter no.)
     */
    private final AnnotationEntry[][] annotsAdd;
    /**
     * The annotations to remove.
     * 
     * (First dimension: parameter no.)
     */
    private final AnnotationEntry[][] annotsRem;
    /**
     * Whether or not the patch describes runtime annotations.
     */
    public final boolean isRuntime;
    
    /**
     * Creates.
     * @param annotsAdd The annotations to add.
     *     (First dimension: parameter no.)
     * @param annotsRem The annotations to remove.
     *     (First dimension: parameter no.)
     * @param isRuntime Whether or not runtime annotations are being
     *     describled.
     */
    public ParameterAnnotationsPatch(AnnotationEntry[][] annotsAdd,
            AnnotationEntry[][] annotsRem, boolean isRuntime){
        this.annotsAdd = annotsAdd;
        this.annotsRem = annotsRem;
        this.isRuntime = isRuntime;
    }
    
    /**
     * Generates.
     * @param old The old attributes.
     * @param new_ The new attributes.
     * @param patches The already generated patches.
     */
    public static void generate(Attribute[] old, Attribute[] new_,
            List<AttributePatch> patches){
        for(boolean isRuntime : new boolean[]{true, false}){
            // Find old Annotations
            ParameterAnnotations oldAnnots = null;
            for(int i = 0; i < old.length; i++){
                boolean correct = false;
                correct |= isRuntime &&
                        old[i] instanceof RuntimeVisibleParameterAnnotations;
                correct |= !isRuntime &&
                        old[i] instanceof RuntimeInvisibleParameterAnnotations;
                if(correct){
                    oldAnnots = (ParameterAnnotations)old[i];
                    break;
                }
            }
            
            //Find new Annotations
            ParameterAnnotations newAnnots = null;
            for(int i = 0; i < new_.length; i++){
                boolean correct = false;
                correct |= isRuntime &&
                        new_[i] instanceof RuntimeVisibleParameterAnnotations;
                correct |= !isRuntime &&
                        new_[i] instanceof
                                RuntimeInvisibleParameterAnnotations;
                if(correct){
                    newAnnots = (ParameterAnnotations)new_[i];
                    break;
                }
            }
            
            ParameterAnnotationEntry[] oldEntries = oldAnnots == null ?
                    new ParameterAnnotationEntry[0] :
                    oldAnnots.getParameterAnnotationEntries();
            ParameterAnnotationEntry[] newEntries = newAnnots == null ?
                    new ParameterAnnotationEntry[0] :
                    newAnnots.getParameterAnnotationEntries();
            
            int paes = oldEntries.length;
            if(newEntries.length > paes)
                paes = newEntries.length;
            
            // Finds entries which are new.
            AnnotationEntry[][] addedEntries = new AnnotationEntry[paes][];
            AnnotationEntry[][] removedEntries = new AnnotationEntry[paes][];
            for(int i = 0; i < paes; i++){
                AnnotationEntry[] oldAes = oldEntries.length > i ?
                        oldEntries[i].getAnnotationEntries() :
                        new AnnotationEntry[0];
                AnnotationEntry[] newAes = newEntries.length > i ?
                        newEntries[i].getAnnotationEntries() :
                        new AnnotationEntry[0];
                
                List<AnnotationEntry> addedAes =
                        new ArrayList<AnnotationEntry>();
                List<AnnotationEntry> removedAes =
                        new ArrayList<AnnotationEntry>();
                
                // Find added entries
                outer: for(AnnotationEntry newAe : newAes){
                    for(AnnotationEntry oldAe : oldAes){
                        if(Util.equals(newAe, oldAe))
                            continue outer;
                    }
                    addedAes.add(newAe);
                }
                
                // Find removed entries
                outer: for(AnnotationEntry oldAe : oldAes){
                    for(AnnotationEntry newAe : newAes){
                        if(Util.equals(newAe, oldAe))
                            continue outer;
                    }
                    removedAes.add(oldAe);
                }
                
                addedEntries[i] = addedAes.toArray(
                        new AnnotationEntry[addedAes.size()]);
                removedEntries[i] = removedAes.toArray(
                        new AnnotationEntry[removedAes.size()]);
            }
            
            patches.add(new ParameterAnnotationsPatch(
                    addedEntries,
                    removedEntries,
                    isRuntime));
        }
    }
    
    @Override
    public List<Attribute> patch(List<Attribute> attribs, CPoolMap map){
        // Get exisiting annotations...
        AnnotationEntry[][] newAnnots =
                new AnnotationEntry[annotsAdd.length][];
        for(int i = 0; i < newAnnots.length; i++){
            newAnnots[i] = new AnnotationEntry[annotsAdd[i].length];
            for(int f = 0; f < newAnnots[i].length; f++)
                newAnnots[i][f] = map.applyTo(annotsAdd[i][f]);
        }
        
        AnnotationEntry[][] remAnnots =
                new AnnotationEntry[annotsRem.length][];
        for(int i = 0; i < remAnnots.length; i++){
            remAnnots[i] = new AnnotationEntry[annotsRem[i].length];
            for(int f = 0; f < remAnnots[i].length; f++)
                remAnnots[i][f] = map.applyTo(annotsRem[i][f]);
        }
        
        int index = findName(
                attribs,
                isRuntime ?
                        "RuntimeVisibleParameterAnnotations" :
                        "RuntimeInvisibleParameterAnnotations");
        AnnotationEntry[][] oldAnnots;
        if(index == -1)
            oldAnnots = new AnnotationEntry[0][0];
        else{
            ParameterAnnotations pa = (ParameterAnnotations)attribs.get(index);
            ParameterAnnotationEntry[] paes =
                    pa.getParameterAnnotationEntries();
            oldAnnots = new AnnotationEntry[pa.getNumParameterAnnotation()][];
            for(int i = 0; i < oldAnnots.length; i++)
                oldAnnots[i] = paes[i].getAnnotationEntries();
        }
        
        int num_paes = newAnnots.length;
        if(oldAnnots.length > num_paes)
            num_paes = oldAnnots.length;
        
        // Unify...
        
        List<ParameterAnnotationEntry> paes =
                new ArrayList<ParameterAnnotationEntry>();
        for(int i = 0; i < num_paes; i++){
            List<AnnotationEntry> aes = new ArrayList<AnnotationEntry>();
            // Add old annotations.
            if(oldAnnots.length > i){
                entries: for(AnnotationEntry oldAe : oldAnnots[i]){
                    if(remAnnots.length > i){
                        for(AnnotationEntry remAe : remAnnots[i]){
                            if(Util.equals(oldAe, remAe))
                                continue entries;
                        }
                    }
                    aes.add(oldAe);
                }
            }
            // Add new annotations
            if(newAnnots.length > i){
                for(AnnotationEntry newAe : newAnnots[i])
                    aes.add(newAe);
            }
            
            // Pack into ParameterAnnotationEntry
            ParameterAnnotationEntry pae = new ParameterAnnotationEntry(
                    aes.toArray(new AnnotationEntry[aes.size()]));
            paes.add(pae);
        }
        
        // Get length
        boolean nonEmptyFound = false;
        int length = 1; // num_parameters
        for(int i = 0; i < paes.size(); i++){
            length += 2; // num_annotations
            for(AnnotationEntry e : paes.get(i).getAnnotationEntries()){
                length += Util.getLength(e);
                nonEmptyFound = true;
            }
        }
        if(!nonEmptyFound)
            return attribs;
        
        // Pack into ParamterAnnotations
        ParameterAnnotations pa;
        if(isRuntime)
            pa = new RuntimeVisibleParameterAnnotations(
                    Util.findConstantStringIn(map,
                            "RuntimeVisibleParameterAnnotations"),
                    length,
                    paes.toArray(new ParameterAnnotationEntry[paes.size()]),
                    map.to);
        else
            pa = new RuntimeInvisibleParameterAnnotations(
                    Util.findConstantStringIn(map,
                            "RuntimeInvisibleParameterAnnotations"),
                    length,
                    paes.toArray(new ParameterAnnotationEntry[paes.size()]),
                    map.to);
        
        return replaceTypeWith(
                attribs,
                isRuntime ?
                        "RuntimeVisibleParameterAnnotations" :
                        "RuntimeInvisibleParameterAnnotions",
                pa);
    }
    
}
